/*
 * Copyright (c) 2012, marco.tamburelli@gmail.com
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met: 
 * 
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer. 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution. 
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.bmi.gwt.mi.server.core.queue;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import org.apache.log4j.Logger;
import org.bmi.gwt.mi.server.channel.ChannelFactory;
import org.bmi.gwt.mi.server.core.event.HandlersManager;
import org.bmi.gwt.mi.server.events.QueueEvent;
import org.bmi.gwt.mi.server.events.QueueEventHandler;
import org.bmi.gwt.mi.shared.communication.attribute.AttributeValue;
import org.bmi.gwt.mi.shared.communication.call.Call;

/**
 * Main interface to access queues and their functionality.
 * 
 * @author marco.tamburelli@gmail.com
 */
public class QueuesManager
{
	static protected Logger logger = Logger.getLogger(QueuesManager.class);
	/**
	 * How often (in seconds) the scheduler check for expired queues to remove.
	 */
	public final static int SCHEDULER_FREQUENCE = 30;

	private Map<String, MiQueue> queues;

	private HandlersManager handlersManager;
	private ChannelFactory channelFactory;

	/* scheduler's fields: */
	private boolean scheduling;
	private ScheduledExecutorService scheduler;

	public QueuesManager()
	{
		this.queues = new HashMap<String, MiQueue>();
	}

	public void setHandlersManager(HandlersManager handlersManager)
	{
		this.handlersManager = handlersManager;
	}

	public ChannelFactory getChannelFactory()
	{
		return channelFactory;
	}

	public void setChannelFactory(ChannelFactory channelFactory)
	{
		this.channelFactory = channelFactory;
	}

	public void startQueueScheduler()
	{
		if (this.scheduling) return;

		this.scheduling = true;

		this.scheduler = Executors.newSingleThreadScheduledExecutor();
		this.scheduler.scheduleAtFixedRate(new Runnable()
		{
			@Override
			public void run()
			{
				logger.info("[QueuesManager] - starting queue scheduler");

				synchronized (QueuesManager.this.queues)
				{
					LinkedList<MiQueue> queues = new LinkedList<MiQueue>(QueuesManager.this.queues.values());

					for (MiQueue queue : queues)
						if (System.currentTimeMillis() - queue.getExpireTime() > 0)
							discardQueue(queue.getName(), false);
				}

				logger.info("[QueuesManager] - ending queue scheduler");
			}
		}, QueuesManager.SCHEDULER_FREQUENCE, QueuesManager.SCHEDULER_FREQUENCE, TimeUnit.SECONDS);
	}

	public void stopQueueScheduler()
	{
		this.scheduling = false;
		this.scheduler.shutdownNow();
	}

	/**
	 * Can be published to any number of queues hierarchically dependent.
	 * Publishing to a single defined queue will provoke the creation of it if
	 * it didn't exist yet. Note that only one queue can be created, trying to
	 * publish to multiple queues (an queue expression like
	 * <code>org.test.*</code> will have effect only on existing queues.
	 * 
	 * @param queueExpr The queue name or expression.
	 * @param calls The list of calls to be published.
	 */
	public void publish(String queueExpr, final List<Call> calls)
	{
		long publishTime = System.currentTimeMillis();

		if (queueExpr.endsWith("*"))
		{
			Set<String> keySet = this.queues.keySet();

			for (String queueName : keySet)
			{
				MiQueue q = this.queues.get(queueName);

				if (q != null && QueueExpressionUtil.match(queueName, queueExpr))
				{
					ArrayList<Call> callList = new ArrayList<Call>(calls.size());

					for (Call call : calls)
						callList.add(call);

					q.publish(callList, publishTime);
				}
			}
		}
		else
		{
			MiQueue q = tryQueue(queueExpr);

			if (q != null)
			{
				ArrayList<Call> callList = new ArrayList<Call>(calls.size());

				for (Call call : calls)
					callList.add(call);

				q.publish(callList, publishTime);
			}
		}
	}

	/**
	 * If doesn't exist yet, adds a queue and starts queue event.
	 * 
	 * @param queueName The name of the queue.
	 * @return Returns the existing queue, or a new queue, or null, if some
	 *         other thread removed the queue before the method returns.
	 */
	public MiQueue tryQueue(String queueName)
	{
		if (!this.queues.containsKey(queueName)) synchronized (this.queues)
		{
			if (!this.queues.containsKey(queueName))
			{
				final MiQueue q = new MiQueue(this.handlersManager, queueName);
				this.queues.put(queueName, q);

				if (this.handlersManager != null) this.handlersManager.fireQueueCreate(q, true);
			}
		}

		/* At this point could be null */
		return this.queues.get(queueName);
	}

	/**
	 * Attempts to discard a queue from the queues list.
	 * 
	 * A queue isn't discarded if has a consuming state, unless the 'force'
	 * parameters is <code>true</code>.
	 * 
	 * @param queueName the name of the queue required to be discarded.
	 * @param force Set to <code>true</code> to discard in any case the queue.
	 */
	private void discardQueue(String queueName, boolean force)
	{
		MiQueue q = this.queues.get(queueName);

		if (q != null && (force || q.isFree()))
		{
			logger.info("[QueuesManager] - discarding queue: " + queueName);

			if (this.handlersManager != null) this.handlersManager.fireQueueDispose(q, false);

			this.queues.remove(queueName);
		}
	}

	/**
	 * This method returns <code>true</code> if the query with provided name has
	 * been created and it's not yet disposed.
	 * 
	 * @param queueName The name of a query.
	 * @return
	 */
	public boolean isQueueAlive(String queueName)
	{
		return this.queues.containsKey(queueName);
	}

	public interface BoundResult
	{
		/**
		 * Returns true if the attempting peer succeeds
		 * 
		 * @return
		 */
		boolean success();

		/**
		 * Fires all queue events connected with creation, bind and update.
		 */
		void fireEvents();
	}

	/**
	 * This method attempts to bind the client peer and returns a
	 * {@link BoundResult}.
	 * 
	 * Note that this method, will open a new queue, in case it didn't yet
	 * exist.
	 * 
	 * This method can fire several events due the possible implicit queue
	 * creation and attributes assignments:
	 * {@link QueueEventHandler#onCreate(QueueEvent)} and
	 * {@link QueueEventHandler#onUpdate(QueueEvent)}
	 * 
	 * @param state This method uses a queue state to try to generate a queue.
	 * 
	 * @return
	 */
	public BoundResult bind(QueueState state)
	{
		synchronized (this.queues)
		{
			final String queueName = state.getQueueName();

			MiQueue q = this.queues.get(queueName);

			final boolean isNull = (q == null);
			final boolean hasAttributes = !state.getAttributes().isEmpty();

			final HandlersManager h = QueuesManager.this.handlersManager;

			/*
			 * Credentials are not checked, because even if are the same, the
			 * same client can't consume more than once the same queue.
			 */
			if (isNull || q.isFree())
			{
				if (isNull)
					q = new MiQueue(this.handlersManager, state);
				else
					q.setState(state, System.currentTimeMillis());

				this.queues.put(queueName, q);

				final MiQueue q1 = q;

				return new BoundResult()
				{
					@Override
					public boolean success()
					{
						return true;
					}

					@Override
					public void fireEvents()
					{
						/*
						 * Each handler should intercept the bound event or
						 * creation and bound, plus the update event in case
						 * there are attributes.
						 */
						if (h != null && isNull) h.fireQueueCreate(q1, true);
						if (h != null) h.fireQueueBound(q1, true);
						if (h != null && hasAttributes) h.fireQueueUpdate(q1, true);
					}
				};
			}

			return new BoundResult()
			{
				@Override
				public boolean success()
				{
					return false;
				}

				@Override
				public void fireEvents()
				{}
			};
		}
	}

	/**
	 * This method attempts to unbind a queue from its owner peer.
	 * 
	 * @param queueName the name of the queue.
	 * @param credentials Peer credentials.
	 * @param discard When <code>true</code> the queue will be discarded now;
	 *        otherwise the queue is just scheduled to be discarded.
	 */
	public void unbind(String queueName, String credentials, boolean discard)
	{
		synchronized (this.queues)
		{
			MiQueue q = this.queues.get(queueName);

			if (q.isOwner(credentials))
			{
				if (discard)
					discardQueue(queueName, true);
				else
					q.free();
			}
		}
	}

	/**
	 * Returns an attribute value from a queue.
	 * 
	 * Note: this method should be used only for queue we know it's exists and
	 * it's not disposed.
	 * 
	 * @param <T> The type for the attribute value
	 * 
	 * @param queueName The name of the target queue.
	 * @param key The attribute key.
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public <T extends AttributeValue<?>> T getQueueAttribute(String queueName, String key)
	{
		/* The queue must exists so no synchronization should be required */
		MiQueue q = this.queues.get(queueName);

		return (T) q.getAttribute(key);
	}

	/**
	 * Sets an attribute value from a queue.
	 * 
	 * Note: this method should be used only for queue we know it's exists and
	 * it's not disposed.
	 * 
	 * @param <T> The type for the attribute value
	 * 
	 * @param queueName The name of the target queue.
	 * @param key The attribute key.
	 * @param value The provided attribute value.
	 * @param credentials User credentials to check if the attribute can be
	 *        updated.
	 * 
	 * @return <code>true</code> if the attribute has been updated.
	 */
	public <T extends AttributeValue<?>> boolean setQueueAttribute(String queueName, String key, T value,
			String credentials)
	{
		/* The queue must exists so no synchronization should be required */
		MiQueue q = this.queues.get(queueName);

		if (q.isOwner(credentials))
		{
			q.setAttribute(key, value);

			final MiQueue q1 = q;

			if (this.handlersManager != null) this.handlersManager.fireQueueUpdate(q1, true);

			return true;
		}

		return false;
	}

	/**
	 * Removes an attribute by its key from a queue.
	 * 
	 * Note: this method should be used only for queue we know it exists and
	 * it's not disposed.
	 * 
	 * @param queueName The name of the target queue.
	 * @param key The attribute key.
	 * @param credentials User credentials to check if the attribute can be
	 *        removed.
	 * 
	 * @return <code>true</code> if the attribute has been updated.
	 */
	public boolean removeQueueAttribute(String queueName, String key, String credentials)
	{
		/* The queue must exists so no synchronization should be required */
		MiQueue q = this.queues.get(queueName);

		if (q.isOwner(credentials)) return false;

		q.removeAttribute(key);

		final MiQueue q1 = q;

		if (this.handlersManager != null) this.handlersManager.fireQueueUpdate(q1, true);

		return true;
	}
}
